In [ ]:
# Console related imports.
from subprocess import Popen, PIPE
import os
from IPython.utils.py3compat import bytes_to_str, string_types

# Widget related imports.
from IPython.html import  widgets
from IPython.display import display

Define function to run a process without blocking the input.


In [ ]:
def read_process(process, append_output):
    """ Try to read the stdout and stderr of a process and render it using 
    the append_output method provided
    
    Parameters
    ----------
    process: Popen handle
    append_output: method handle
        Callback to render output.  Signature of
        append_output(output, [prefix=])"""
    
    try:
        stdout = process.stdout.read()
        if stdout is not None and len(stdout) > 0:
            append_output(stdout, prefix='    ')
    except:
        pass
        
    try:
        stderr = process.stderr.read()
        if stderr is not None and len(stderr) > 0:
            append_output(stderr, prefix='ERR ')
    except:
        pass


def set_pipe_nonblocking(pipe):
    """Set a pipe as non-blocking"""
    try:
        import fcntl
        fl = fcntl.fcntl(pipe, fcntl.F_GETFL)
        fcntl.fcntl(pipe, fcntl.F_SETFL, fl | os.O_NONBLOCK)
    except:
        pass

kernel = get_ipython().kernel
def run_command(command, append_output, has_user_exited=None):
    """Run a command asyncronously
    
    Parameters
    ----------
    command: str
        Shell command to launch a process with.
    append_output: method handle
        Callback to render output.  Signature of
        append_output(output, [prefix=])
    has_user_exited: method handle
        Check to see if the user wants to stop the command.
        Must return a boolean."""
    
    # Echo input.
    append_output(command, prefix='>>> ')
    
    # Create the process.  Make sure the pipes are set as non-blocking.
    process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
    set_pipe_nonblocking(process.stdout)
    set_pipe_nonblocking(process.stderr)
    
    # Only continue to read from the command 
    while (has_user_exited is None or not has_user_exited()) and process.poll() is None:
        read_process(process, append_output)
        kernel.do_one_iteration() # Run IPython iteration.  This is the code that
                                  # makes this operation non-blocking.  This will
                                  # allow widget messages and callbacks to be 
                                  # processed.
        
    # If the process is still running, the user must have exited.
    if process.poll() is None:
        process.kill()
    else:
        read_process(process, append_output) # Read remainer

Create the console widgets without displaying them.


In [ ]:
console_container = widgets.VBox(visible=False)
console_container.padding = '10px'

output_box = widgets.Textarea()
output_box.height = '400px'
output_box.font_family = 'monospace'
output_box.color = '#AAAAAA'
output_box.background_color = 'black'
output_box.width = '800px'

input_box = widgets.Text()
input_box.font_family = 'monospace'
input_box.color = '#AAAAAA'
input_box.background_color = 'black'
input_box.width = '800px'

console_container.children = [output_box, input_box]

Hook the process execution methods up to our console widgets.


In [ ]:
def append_output(output, prefix):
    if isinstance(output, string_types):
        output_str = output
    else:
        output_str = bytes_to_str(output)
    output_lines = output_str.split('\n')
    formatted_output = '\n'.join([prefix + line for line in output_lines if len(line) > 0]) + '\n'
    output_box.value += formatted_output
    output_box.scroll_to_bottom()
    
def has_user_exited():
    return not console_container.visible

def handle_input(sender):
    sender.disabled = True
    try:
        command = sender.value
        sender.value = ''
        run_command(command, append_output=append_output, has_user_exited=has_user_exited)
    finally:
        sender.disabled = False
        
input_box.on_submit(handle_input)

Create the button that will be used to display and hide the console. Display both the console container and the new button used to toggle it.


In [ ]:
toggle_button = widgets.Button(description="Start Console")
def toggle_console(sender):
    console_container.visible = not console_container.visible
    if console_container.visible:
        toggle_button.description="Stop Console"
        input_box.disabled = False
    else:
        toggle_button.description="Start Console"
toggle_button.on_click(toggle_console)

display(toggle_button)
display(console_container)